﻿using System;
using System.Collections.Generic;
using System.Common.IO;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;

namespace System.Common.Highliter
{
  public class FirebirdSqlFormatProvider : ISqlFormatProvider
  {
    #region  ... enums statics fields and  constants...

    private const char APOSTROPHE = '\'';
    private const char QUOTATION_MARK = '"';
    private const char NULL = '\0';
    private const char MAX = '\xff';
    private const char BS = '\\';
    private const char CR = '\r';
    private const char LF = '\n';
    private const char SPACE = (char)32;

    #endregion

    #region ... constructors...

    public FirebirdSqlFormatProvider()
    {
      FillTable( fFunctions, TokenType.Function, FbConst.FIREBIRDFUNCTIONS );
      FillTable( fKeywords, TokenType.KeyWord, FbConst.FIREBIRDKEYWORDS );
      FillTable( fTypes, TokenType.Datatype, FbConst.FIREBIRDTYPES );

      FillMethodTables();
      MakeIdentTable();
    }

    #endregion

    #region ... properties indexers ...

    public TokenType TokenId { get; private set; }
    public string Token { get; private set; }
    public RangeState Range { get; private set; }
    public int TokenPos { get; private set; }

    #endregion

    #region ...public methods...

    public void SetText( string source, bool wordOnly = false )
    {
      WordOnly = wordOnly;

      if ( fReader != null )
        DisposeReader();

      fReader = ScriptReader.CreateFromString( source );

      TokenPos = 0;
      fNextTokenPos = 0;
      ReadNextChar();
    }

    public void AddTables( string tableList )
    {
      FillTable( fTables, TokenType.TableName, tableList );
    }

    public void AddTable( string tablename )
    {
      var key = tablename.Trim().ToUpper().GetHashCode();
      if ( fTables.ContainsKey( key ) )
        return;

      fTables.Add( key, new TokenEntry( tablename, TokenType.TableName ) );
    }

    public void AddViews( string viewList )
    {
      FillTable( fViews, TokenType.ViewName, viewList );
    }

    public void AddView( string viewName )
    {
      var key = viewName.Trim().ToUpper().GetHashCode();
      if ( fViews.ContainsKey( key ) )
        return;

      fViews.Add( key, new TokenEntry( viewName, TokenType.TableName ) );
    }

    public void AddDomains( string domainList )
    {
      FillTable( fDomains, TokenType.Domain, domainList );
    }

    public void AddDomain( string domainName )
    {
      var key = domainName.Trim().ToUpper().GetHashCode();
      if ( fDomains.ContainsKey( key ) )
        return;

      fDomains.Add( key, new TokenEntry( domainName, TokenType.TableName ) );
    }

    public void Next()
    {
      if ( Eof )
        return;

      TokenPos = fNextTokenPos;

      switch ( Range )
      {
      case RangeState.Comment:
      case RangeState.ConditionalComment:
        AnsiCProc();
        break;

      case RangeState.String:
        AsciiCharProc();
        break;

      default:
        fProcessTable[CurrentChar]();
        break;
      }

      var current_pos = fReader.Position;
      var token_size = current_pos - TokenPos - 1;

      if ( token_size <= 0 )
        Token = string.Empty;
      else
      {
        var buffer = new char[token_size];
        fReader.Seek( TokenPos, SeekOrigin.Begin );

        fReader.Read( buffer, 0, token_size );
        fReader.Seek( current_pos, SeekOrigin.Begin );

        Token = new string( buffer );
      }

      fNextTokenPos = fReader.Position - 1;
    }

    #endregion

    #region ...private methods...

    private void AnsiCProc()
    {
      switch ( CurrentChar )
      {
      case NULL:
        NullProc();
        break;

      case LF:
        LfProc();
        break;

      case CR:
        CrProc();
        break;

      default:
        TokenId = Range == RangeState.ConditionalComment ? TokenType.ConditionalComment : TokenType.Comment;
        ScanEndOfComment();
        break;
      }
    }

    private void DisposeReader()
    {
      TokenPos = 0;
      TokenId = TokenType.Unknown;
      Range = RangeState.Unknown;

      fReader.Dispose();
      fReader = null;
    }

    private void FillMethodTables()
    {
      for ( var ch = NULL; ch <= MAX; ch++ )
      {
        if ( ch == 0 )
        {
          fProcessTable[ch] = NullProc;
          continue;
        }

        if ( ch == 10 )
        {
          fProcessTable[ch] = LfProc;
          continue;
        }

        if ( ch == 13 )
        {
          fProcessTable[ch] = CrProc;
          continue;
        }

        if ( ch == APOSTROPHE ) // '
        {
          fProcessTable[ch] = AsciiCharProc;
          continue;
        }

        if ( ch == '=' )
        {
          fProcessTable[ch] = EqualProc;
          continue;
        }

        if ( ch == '>' )
        {
          fProcessTable[ch] = GreaterProc;
          continue;
        }

        if ( ch == '>' )
        {
          fProcessTable[ch] = LowerProc;
          continue;
        }

        if ( ch == '-' )
        {
          fProcessTable[ch] = MinusProc;
          continue;
        }

        if ( ch == '#' )
        {
          fProcessTable[ch] = HashProc;
          continue;
        }

        if ( ch == '|' )
        {
          fProcessTable[ch] = OrSymbolProc;
          continue;
        }

        if ( ch == '+' )
        {
          fProcessTable[ch] = PlusProc;
          continue;
        }

        if ( ch == '/' )
        {
          fProcessTable[ch] = SlashProc;
          continue;
        }

        if ( ch == '&' )
        {
          fProcessTable[ch] = AndSymbolProc;
          continue;
        }

        if ( ch == QUOTATION_MARK )
        {
          fProcessTable[ch] = QuoteProc;
          continue;
        }

        if ( ch == '`' )
        {
          fProcessTable[ch] = BacktickProc;
          continue;
        }

        if ( ch == '[' )
        {
          fProcessTable[ch] = BracketProc;
          continue;
        }

        if ( ch == ':' || ch == '@' )
        {
          fProcessTable[ch] = VariableProc;
          continue;
        }

        if ( ( ch >= 'A' && ch <= 'Z' ) || ( ch >= 'a' && ch <= 'z' ) || ch == '_' )
        {
          fProcessTable[ch] = IdentProc;
          continue;
        }

        if ( ch >= '0' && ch <= '9' )
        {
          fProcessTable[ch] = NumberProc;
          continue;
        }

        if ( ( ch > 0 && ch < 10 ) || ch == 11 || ch == 12 || ( ch > 13 && ch < 33 ) )
        {
          fProcessTable[ch] = SpaceProc;
          continue;
        }

        if ( ch == '^' || ch == '%' || ch == '*' || ch == '!' )
        {
          fProcessTable[ch] = SymbolAssignProc;
          continue;
        }

        if ( ch == '{' || ch == '}' || ch == '.' || ch == ',' || ch == ';' || ch == '?' || ch == '(' || ch == ')' || ch == ']' ||
             ch == '~' )
        {
          fProcessTable[ch] = SymbolProc;
          continue;
        }

        fProcessTable[ch] = UnknownProc;
      }
    }

    private void FillTable( SortedList<int, TokenEntry> table, TokenType tkType, string source )
    {
      table.Clear();

      foreach ( var entry in source.Split( ',' ).
                                    Select( value=>new TokenEntry( value.Trim(), tkType ) ) )
        table.Add( entry.GetHashCode(), entry );
    }

    private string GetKeywordFromCurrentPos()
    {
      var sb = new StringBuilder();
      var index = 0;

      while ( !Eof && fIdentifierChars[index].IndexOf( CurrentChar ) != -1 )
      {
        sb.Append( CurrentChar );
        ReadNextChar();
        index = 1;
      }

      return sb.ToString();
    }

    [SuppressMessage( "ReSharper", "ConvertIfStatementToReturnStatement" )]
    private TokenType IdentifierType( out int keyLen )
    {
      TokenEntry entry = null;

      var prev_ch = PrevChar;

      var keyword = GetKeywordFromCurrentPos();
      keyLen = keyword.Length;
      var key = keyword.ToUpper().GetHashCode();

      if ( fKeywords.TryGetValue( key, out entry ) )
        return entry.TokenType;

      if ( IsCurrentCharIs( '(' ) && fFunctions.TryGetValue( key, out entry ) )
        return entry.TokenType;

      if ( ( WordOnly || prev_ch == SPACE ) && fTypes.TryGetValue( key, out entry ) )
        return entry.TokenType;

      if ( ( WordOnly || IsCharIs( prev_ch, SPACE, ',' ) && IsCurrentCharIs( ',', ';', NULL, CR, LF, SPACE ) ) &&
           fTables.TryGetValue( key, out entry ) )
        return entry.TokenType;

      if ( ( WordOnly || IsCharIs( prev_ch, SPACE, ',' ) && IsCurrentCharIs( ',', ';', NULL, CR, LF, SPACE ) ) &&
           fViews.TryGetValue( key, out entry ) )
        return entry.TokenType;

      if ( fDomains.TryGetValue( key, out entry ) )
        return entry.TokenType;

      return TokenType.Identifier;
    }

    private void IdentProc()
    {
      int key_len;
      TokenId = IdentifierType( out key_len );

      if ( TokenId == TokenType.Comment )
        ScanForCrlf();
      else
        for ( ; fIdentifierChars[1].IndexOf( CurrentChar ) != -1; ReadNextChar() ) {}
    }

    private static bool IsCharIs( int chr, params char[] chars )
    {
      if ( chr == -1 )
        return false;

      foreach ( var ch in chars )
      {
        if ( chr == ch )
          return true;
      }

      return false;
    }

    private bool IsCurrentCharIs( params char[] chars )
    {
      return IsCharIs( fReader.Current, chars );
    }

    private bool IsNextCharIs( params char[] chars )
    {
      return IsCharIs( fReader.Next, chars );
    }

    private void MakeIdentTable()
    {
      var sb = new StringBuilder();
      for ( var ch = 'a'; ch <= 'z'; ch++ )
        sb.Append( ch );

      for ( var ch = 'A'; ch <= 'Z'; ch++ )
        sb.Append( ch );

      sb.Append( '_' );
      sb.Append( '$' );

      fIdentifierChars = new string[2];
      fIdentifierChars[0] = sb.ToString();

      sb.Append( "0123456789" );
      fIdentifierChars[1] = sb.ToString();
    }

    private void ScanEndOfComment()
    {
      fReader.ReadAtSeparator( "*/" );
      Range = RangeState.Unknown;
    }

    private void ScanForCrlf()
    {
      fReader.ReadAtEol();
    }

    #endregion

    #region ..process methods ...

    private void AndSymbolProc()
    {
      TokenId = TokenType.Symbol;
      ReadNextChar();

      if ( IsCurrentCharIs( '=', '&' ) )
        ReadNextChar();
    }

    private void AsciiCharProc()
    {
      if ( IsCurrentCharIs( NULL ) )
      {
        NullProc();
        return;
      }

      TokenId = TokenType.String;

      if ( Range != RangeState.String || ( CurrentChar != APOSTROPHE || PrevChar != BS ) )
      {
        Range = RangeState.String;
        do
        {
          if ( !Eof && CurrentChar != BS && NextChar == APOSTROPHE )
          {
            ReadNextChar();
            break;
          }
          ReadNextChar();
        } while ( !Eof && CurrentChar != NULL && CurrentChar != CR && CurrentChar != LF );
      }

      if ( Eof )
        return;

      if ( CurrentChar != APOSTROPHE || PrevChar == BS )
        return;

      ReadNextChar();
      Range = RangeState.Unknown;
    }

    private void CrProc()
    {
      TokenId = TokenType.CRLF;
      ReadNextChar();

      if ( !IsCurrentCharIs( LF ) )
        return;

      ReadNextChar();
    }

    private void LfProc()
    {
      TokenId = TokenType.CRLF;
      ReadNextChar();
    }

    private void EqualProc()
    {
      TokenId = TokenType.Symbol;
      ReadNextChar();

      if ( IsCurrentCharIs( '=', '>' ) )
        ReadNextChar();
    }

    private void GreaterProc()
    {
      TokenId = TokenType.Symbol;
      ReadNextChar();

      if ( IsCurrentCharIs( '=', '>' ) )
        ReadNextChar();
    }

    private void LowerProc()
    {
      TokenId = TokenType.Symbol;
      ReadNextChar();
      switch ( CurrentChar )
      {
      case '=':
        ReadNextChar();
        break;

      case '<':
        ReadNextChar();
        if ( CurrentChar == '=' )
          ReadNextChar();

        break;
      }
    }

    private void MinusProc()
    {
      ReadNextChar();
      if ( CurrentChar == '-' )
      {
        TokenId = TokenType.Comment;
        ScanForCrlf();
      }
      else
        TokenId = TokenType.Symbol;
    }

    private void PlusProc()
    {
      TokenId = TokenType.Symbol;
      ReadNextChar();
      if ( IsCurrentCharIs( '=', '+' ) )
        ReadNextChar();
    }

    private void HashProc()
    {
      ReadNextChar();
      TokenId = TokenType.Unknown;
    }

    private void NullProc()
    {
      TokenId = TokenType.Null;
    }

    private void NumberProc()
    {
      ReadNextChar();
      TokenId = TokenType.Number;

      while ( char.IsDigit( CurrentChar ) || CurrentChar == '.' || CurrentChar == '-' )
      {
        if ( IsNextCharIs( '.' ) )
          break;

        ReadNextChar();
      }
    }

    private void OrSymbolProc()
    {
      TokenId = TokenType.Symbol;
      ReadNextChar();

      if ( IsCurrentCharIs( '=', '|' ) )
        ReadNextChar();
    }

    private void SlashProc()
    {
      ReadNextChar();
      switch ( CurrentChar )
      {
      case '*':
        Range = RangeState.Comment;
        TokenId = TokenType.Comment;
        ScanEndOfComment();
        break;

      case '=':
        ReadNextChar();
        TokenId = TokenType.Symbol;
        break;

      default:
        TokenId = TokenType.Symbol;
        break;
      }
    }

    private void SpaceProc()
    {
      TokenId = TokenType.Space;
      while ( !Eof && CurrentChar <= SPACE && !IsCurrentCharIs( NULL, LF, CR ) )
        ReadNextChar();
    }

    private void QuoteProc()
    {
      TokenId = TokenType.DelimitedIdentifier;
      fReader.ReadAtSeparator( QUOTATION_MARK );
    }

    private void BacktickProc()
    {
      ReadNextChar();
      TokenId = TokenType.Unknown;
    }

    private void BracketProc()
    {
      ReadNextChar();
      TokenId = TokenType.Symbol;
    }

    private void SymbolProc()
    {
      ReadNextChar();
      TokenId = TokenType.Symbol;
    }

    private void SymbolAssignProc()
    {
      TokenId = TokenType.Symbol;
      ReadNextChar();
      if ( CurrentChar == '=' )
        ReadNextChar();
    }

    private void VariableProc()
    {
      if ( CurrentChar == '@' || CurrentChar == ':' )
      {
        SymbolProc();
        return;
      }

      TokenId = TokenType.Variable;
      ReadNextChar();

      while ( !Eof && fIdentifierChars[1].IndexOf( CurrentChar ) != -1 )
        ReadNextChar();
    }

    private void UnknownProc()
    {
      ReadNextChar();
      TokenId = TokenType.Unknown;
    }

    #endregion

    #region -- reader wrapper --

    public bool Eof
    {
      get { return fReader == null || fReader.Eof; }
    }

    internal bool WordOnly { get; set; }

    private char PrevChar
    {
      get { return (char)fReader.Prev; }
    }

    private char CurrentChar
    {
      get { return (char)fReader.Current; }
    }

    private char NextChar
    {
      get { return (char)fReader.Next; }
    }

    private void ReadNextChar()
    {
      fReader.NextChar();
    }

    #endregion

    #region -- fields --

    private readonly SortedList<int, TokenEntry> fKeywords = new SortedList<int, TokenEntry>();
    private readonly SortedList<int, TokenEntry> fFunctions = new SortedList<int, TokenEntry>();
    private readonly SortedList<int, TokenEntry> fTypes = new SortedList<int, TokenEntry>();
    private readonly SortedList<int, TokenEntry> fTables = new SortedList<int, TokenEntry>();
    private readonly SortedList<int, TokenEntry> fViews = new SortedList<int, TokenEntry>();
    private readonly SortedList<int, TokenEntry> fDomains = new SortedList<int, TokenEntry>();
    private readonly SortedList<char, Action> fProcessTable = new SortedList<char, Action>();
    private string[] fIdentifierChars;
    private ScriptReader fReader;
    private int fNextTokenPos;

    #endregion

    #region Implementation of IDisposable

    public void Dispose()
    {
      if ( fReader != null )
        DisposeReader();
    }

    #endregion
  }
}
